﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Media.Imaging;
using Balloonerds.Algebra;

namespace RayTracer
{
    public class Scene
    {
        private const double precision = 1e-6;
        private const double intensityThreshold = 1e-3;

        public Color Ambient { get; set; }
        public Color HorizonColor { get; set; }
        public Color ZenithColor { get; set; }
        public List<Thing> Objects { get; set; }
        public List<Light> Lights { get; set; }
        public Camera Camera { get; set; }
        public int MaxReflectionDepth { get; set; }

        public Scene()
        {
            Ambient = .3 * Color.White;
            HorizonColor = Color.Black;
            ZenithColor = Color.Black;
            Objects = new List<Thing>();
            Lights = new List<Light>();
            Camera = new Camera(new Vector(50, 2, 0), new Vector(0, 0, 0));
            MaxReflectionDepth = 3;
        }

        public Color GetPixelColor(int x, int y)
        {
            return TraceRay(Camera.Cast(x, y));
        }
        public BitmapSource Render()
        {
            var bitmap = Camera.Sensor.GetBitmap();
            for (var y = 0; y < bitmap.Height; y++)
                for (var x = 0; x < bitmap.Width; x++)
                    bitmap.WritePixels(new System.Windows.Int32Rect(x, y, 1, 1), GetPixelColor(x, y).ToByteArray(), 4, 0);

            return bitmap;
        }

        private Color TraceRay(Ray ray, int depth = 0)
        {
            var cosAzimuth = ray.Direction * Vector.yAxis;
            var color = cosAzimuth * ZenithColor + (1.0 - cosAzimuth) * HorizonColor;

            var isect = GetIntersection(ray);
            if (isect != null)
            {
                //ambient light color
                color = Ambient * isect.Object.Surface.Diffuse(isect.Point);

                //lighting color
                foreach (var light in Lights)
                {
                    var ol = light.Position - isect.Point;
                    var olNorm = ol.Normalized;
                    var olRay = new Ray()
                    {
                        Origin = isect.Point,
                        Direction = olNorm
                    };

                    if (!light.IsIlluminating(olRay) ||
                        TestIntersection(olRay, ol.Norm))
                        continue;

                    //diffuse lighting
                    var diffuse = isect.SurfaceNormal * olNorm;
                    if (diffuse > intensityThreshold)
                    {
                        var dcolor = light.Color * diffuse;
                        color += dcolor * isect.Object.Surface.Diffuse(isect.Point);
                    }

                    //specular lighting
                    var specular = olNorm * isect.Ray.Direction.Reflect(isect.SurfaceNormal);
                    if (specular > intensityThreshold)
                    {
                        var scolor = light.Color * Math.Pow(specular, isect.Object.Surface.Shininess(isect.Point));
                        color += scolor * isect.Object.Surface.Specular(isect.Point);
                    }
                }

                //reflection color
                if (depth < MaxReflectionDepth)
                {
                    var reflection = isect.Object.Surface.Reflection(isect.Point);
                    if (reflection != Color.Black)
                    {
                        var reflectedRay = new Ray()
                        {
                            Direction = ray.Direction.Reflect(isect.SurfaceNormal),
                            Origin = isect.Point
                        };

                        var rcolor = TraceRay(reflectedRay, depth + 1);
                        color += rcolor * reflection;
                    }
                }
            }

            return color;
        }
        private Intersection GetIntersection(Ray ray)
        {
            return
                Objects
                    .Select(o => o.GetIntersection(ray))
                    .Where(i => i != null && i.Distance > precision)
                    .OrderBy(i => i.Distance)
                    .FirstOrDefault();
        }
        private bool TestIntersection(Ray ray, double maxDistance)
        {
            return
                Objects
                    .Select(o => o.TestIntersection(ray, maxDistance))
                    .FirstOrDefault(b => b == true)
                    == true;
        }
    }
}
